/*
This file is part of the 3dengfx, realtime visualization system.

Copyright (c) 2004, 2005 John Tsiombikas <nuclear@siggraph.org>

3dengfx is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

3dengfx is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with 3dengfx; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*/

/* higher level 3d object abstraction
 *
 * Author: John Tsiombikas 2004
 * Modified: John Tsiombikas 2005
 */

#include "3dengfx_config.h"

#include "opengl.h"
#include "object.hpp"
#include "3denginefx.hpp"
#include "camera.hpp"
#include "gfxprog.hpp"
#include "texman.hpp"
#include "ggen.hpp"
#include "common/err_msg.h"

RenderParams::RenderParams() {
	billboarded = false;
	zwrite = true;
	blending = false;
	src_blend = BLEND_SRC_ALPHA;
	dest_blend = BLEND_ONE_MINUS_SRC_ALPHA;
	handle_blending = true;
	gfxprog = 0;
	auto_cube_maps = false;
	hidden = false;
	show_normals = false;
	show_normals_scale = 0.5;
	use_vertex_color = false;
	taddr = TEXADDR_WRAP;
	auto_normalize = false;
}
	

Object::Object() {
	bvol_valid = false;
	bvol = 0;
	set_dynamic(false);
}

Object::Object(const TriMesh &mesh) {
	bvol = 0;
	set_mesh(mesh);
	set_dynamic(false);
}

void Object::set_mesh(const TriMesh &mesh) {
	this->mesh = mesh;
	update_bounding_volume();
}

TriMesh *Object::get_mesh_ptr() {
	bvol_valid = false;
	return &mesh;
}

TriMesh &Object::get_mesh() {
	return mesh;
}

const TriMesh &Object::get_mesh() const {
	return mesh;
}

unsigned long Object::get_vertex_count() const {
	return mesh.get_vertex_array()->get_count();
}

const Vertex *Object::get_vertex_data() const {
	return mesh.get_vertex_array()->get_data();
}

Vertex *Object::get_mod_vertex_data() {
	return mesh.get_mod_vertex_array()->get_mod_data();
}

unsigned long Object::get_triangle_count() const {
	return mesh.get_triangle_array()->get_count();
}

const Triangle *Object::get_triangle_data() const {
	return mesh.get_triangle_array()->get_data();
}

Triangle *Object::get_mod_triangle_data() {
	return mesh.get_mod_triangle_array()->get_mod_data();
}

void Object::set_dynamic(bool enable) {
	const_cast<VertexArray*>(mesh.get_vertex_array())->set_dynamic(enable);
	const_cast<TriangleArray*>(mesh.get_triangle_array())->set_dynamic(enable);
	//const_cast<IndexArray*>(mesh.get_index_array())->set_dynamic(enable);
}

bool Object::get_dynamic() const {
	return mesh.get_vertex_array()->get_dynamic();
}

void Object::set_material(const Material &mat) {
	this->mat = mat;
}

Material *Object::get_material_ptr() {
	return &mat;
}

Material Object::get_material() const {
	return mat;
}

void Object::set_render_params(const RenderParams &rp) {
	render_params = rp;
}

RenderParams Object::get_render_params() const {
	return render_params;
}

void Object::set_shading(ShadeMode shading_mode) {
	mat.shading = shading_mode;
}

void Object::set_billboarding(bool enable) {
	render_params.billboarded = enable;
}

void Object::set_zwrite(bool enable) {
	render_params.zwrite = enable;
}

void Object::set_blending(bool enable) {
	render_params.blending = enable;
}

void Object::set_blending_mode(BlendingFactor sblend, BlendingFactor dblend) {
	render_params.src_blend = sblend;
	render_params.dest_blend = dblend;
}

void Object::set_handle_blending(bool enable) {
	render_params.handle_blending = enable;
}

void Object::set_wireframe(bool enable) {
	mat.wireframe = enable;
}

void Object::set_gfx_program(GfxProg *prog) {
	render_params.gfxprog = prog;
}

void Object::set_auto_cube_maps(bool enable) {
	render_params.auto_cube_maps = enable;
}

void Object::set_hidden(bool enable) {
	render_params.hidden = enable;
}

void Object::set_show_normals(bool enable) {
	render_params.show_normals = enable;
}

void Object::set_show_normals_scale(scalar_t scale) {
	render_params.show_normals_scale = scale;
}

void Object::set_auto_global(bool enable) {
	mat.auto_refl = enable;
}

void Object::set_use_vertex_color(bool enable) {
	render_params.use_vertex_color = enable;
}

void Object::set_texture_addressing(TextureAddressing taddr) {
	render_params.taddr = taddr;
}

void Object::set_auto_normalize(bool enable) {
	render_params.auto_normalize = enable;
}

void Object::apply_xform(unsigned long time) {
	world_mat = get_prs(time).get_xform_matrix();
	mesh.apply_xform(world_mat);
	reset_xform(time);
}

void Object::calculate_normals() {
	mesh.calculate_normals();
}

void Object::normalize_normals() {
	mesh.normalize_normals();
}

bool Object::render(unsigned long time) {
	world_mat = get_prs(time).get_xform_matrix();

	if(!bvol_valid) update_bounding_volume();

	// set the active world-space transformation for the bounding volume ...
	bvol->set_transform(world_mat);
	
	/* if we have the camera that generated the active view matrix available
	 * chances are it already has the view frustum, so use it directly to test
	 * the object, otherwise generate one.
	 */
	if(engfx_state::view_mat_camera) {
		if(!bvol->visible(engfx_state::view_mat_camera->get_frustum())) return false;
	} else {
		Matrix4x4 view_proj = engfx_state::proj_matrix * engfx_state::view_matrix;
		
		FrustumPlane frustum[6];
		for(int i=0; i<6; i++) {
			frustum[i] = FrustumPlane(view_proj, i);
		}

		if(!bvol->visible(frustum)) return false;
	}
	
	
	set_matrix(XFORM_WORLD, world_mat);
	mat.set_glmaterial();

	::set_auto_normalize(render_params.auto_normalize);
	
	//render8tex_units();
	render_hack(time);

	if(render_params.auto_normalize) ::set_auto_normalize(false);

	return true;
}

void Object::render_hack(unsigned long time) {
	//::set_material(mat);
	int tex_unit = 0;

	if(mat.tex[TEXTYPE_BUMPMAP]) {
		setup_bump_light(time);	// sets the light vector into texcoord[1]

		set_texture(tex_unit, mat.tex[TEXTYPE_BUMPMAP]);
		enable_texture_unit(tex_unit);
		set_texture_coord_index(tex_unit, 0);
		set_texture_unit_color(tex_unit, TOP_REPLACE, TARG_TEXTURE, TARG_TEXTURE);
		set_texture_unit_alpha(tex_unit, TOP_REPLACE, TARG_PREV, TARG_PREV);
		tex_unit++;
		
		select_texture_unit(tex_unit);
		set_texture(tex_unit, get_normal_cube());
		enable_texture_unit(tex_unit);
		set_texture_coord_index(tex_unit, 1);	// tex coord with the light vector (UVW)
		set_texture_unit_color(tex_unit, TOP_DOT3, TARG_TEXTURE, TARG_PREV);
		set_texture_unit_alpha(tex_unit, TOP_REPLACE, TARG_PREV, TARG_PREV);
		tex_unit++;
	}
	
	if(mat.tex[TEXTYPE_DIFFUSE]) {
		set_texture(tex_unit, mat.tex[TEXTYPE_DIFFUSE]);
		enable_texture_unit(tex_unit);
		set_texture_coord_index(tex_unit, 0);
		set_texture_unit_color(tex_unit, TOP_MODULATE, TARG_TEXTURE, TARG_PREV);
		set_texture_unit_alpha(tex_unit, TOP_MODULATE, TARG_TEXTURE, TARG_PREV);
		//tex_id = mat.tex[TEXTYPE_DIFFUSE]->tex_id;
		::set_texture_addressing(tex_unit, render_params.taddr, render_params.taddr);
		set_matrix(XFORM_TEXTURE, mat.tmat[TEXTYPE_DIFFUSE], 0);
		tex_unit++;
	}
	
	if(mat.tex[TEXTYPE_ENVMAP]) {
		set_texture(tex_unit, mat.tex[TEXTYPE_ENVMAP]);
		enable_texture_unit(tex_unit);
		set_texture_unit_color(tex_unit, TOP_ADD, TARG_TEXTURE, TARG_PREV);
		set_texture_unit_alpha(tex_unit, TOP_REPLACE, TARG_PREV, TARG_TEXTURE);

		if(mat.tex[TEXTYPE_ENVMAP]->get_type() == TEX_CUBE) {
			glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_REFLECTION_MAP);
			glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_REFLECTION_MAP);
			glTexGeni(GL_R, GL_TEXTURE_GEN_MODE, GL_REFLECTION_MAP);
			glEnable(GL_TEXTURE_GEN_S);
			glEnable(GL_TEXTURE_GEN_T);
			glEnable(GL_TEXTURE_GEN_R);

			Matrix4x4 inv_view = engfx_state::view_matrix;
			inv_view[0][3] = inv_view[1][3] = inv_view[2][3] = 0.0;
			inv_view.transpose();

			set_matrix(XFORM_TEXTURE, inv_view, tex_unit);

			::set_texture_addressing(tex_unit, TEXADDR_CLAMP, TEXADDR_CLAMP);
		} else {
			glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_SPHERE_MAP);
			glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_SPHERE_MAP);
			glEnable(GL_TEXTURE_GEN_S);
			glEnable(GL_TEXTURE_GEN_T);

			// TODO: fix this to produce the correct orientation
			/*glMatrixMode(GL_TEXTURE);
			glLoadIdentity();
			glRotatef(180.0, 0.0, 1.0, 0.0);*/
		}
		//tex_id = mat.tex[TEXTYPE_ENVMAP]->tex_id;
		tex_unit++;
	}

	::set_zwrite(render_params.zwrite);
	set_shading_mode(mat.shading);

	if(render_params.handle_blending) {
		if(mat.alpha < 1.0 - small_number) {
			set_alpha_blending(true);
			set_blend_func(BLEND_SRC_ALPHA, BLEND_ONE_MINUS_SRC_ALPHA);
		}
	} else {
		set_alpha_blending(render_params.blending);
		set_blend_func(render_params.src_blend, render_params.dest_blend);
	}

	if(mat.wireframe) ::set_wireframe(true);

	if(render_params.gfxprog) ::set_gfx_program(render_params.gfxprog);

	if(mat.two_sided) set_backface_culling(false);
	if(render_params.use_vertex_color) ::use_vertex_colors(true);
	
	draw(*mesh.get_vertex_array(), *mesh.get_index_array());

	if(render_params.use_vertex_color) ::use_vertex_colors(false);
	if(mat.two_sided) set_backface_culling(true);

	if(mat.wireframe) ::set_wireframe(false);
	if((render_params.handle_blending && mat.alpha < 1.0 - small_number) || 
			(!render_params.handle_blending && render_params.blending)) {
		set_alpha_blending(false);
	}
	if(!render_params.zwrite) ::set_zwrite(true);
	if(mat.shading == SHADING_FLAT) set_shading_mode(SHADING_GOURAUD);


	for(int i=0; i<tex_unit; i++) {
		disable_texture_unit(i);
		glDisable(GL_TEXTURE_GEN_S);
		glDisable(GL_TEXTURE_GEN_T);
		glDisable(GL_TEXTURE_GEN_R);
		glMatrixMode(GL_TEXTURE);
		glLoadIdentity();
		glMatrixMode(GL_MODELVIEW);
		set_matrix(XFORM_TEXTURE, Matrix4x4::identity_matrix, i);
		::set_texture_addressing(tex_unit, TEXADDR_WRAP, TEXADDR_WRAP);
	}

	if(render_params.show_normals) {
		draw_normals();
	}
	if(render_params.gfxprog) ::set_gfx_program(0);
}

void Object::draw_normals() {
	scalar_t normal_scale = mesh.get_vertex_stats().avg_dist * render_params.show_normals_scale;
	int vcount = mesh.get_vertex_array()->get_count();
	const Vertex *vptr = mesh.get_vertex_array()->get_data();

	set_lighting(false);
	
	glBegin(GL_LINES);
	glColor4f(1.0, 1.0, 1.0, 1.0);
	for(int i=0; i<vcount; i++) {
		Vector3 pos = vptr->pos;
		Vector3 normal = vptr->normal * normal_scale;
		
		glVertex3f(pos.x, pos.y, pos.z);
		glVertex3f(pos.x + normal.x, pos.y + normal.y, pos.z + normal.z);
		vptr++;
	}
	glEnd();

	set_lighting(true);
}

void Object::setup_bump_light(unsigned long time) {
	Vector3 lpos = engfx_state::bump_light->get_prs(time).position;

	Matrix4x4 inv_world = world_mat.inverse();
	lpos.transform(inv_world);

	VertexArray *va = mesh.get_mod_vertex_array();
	int vcount = va->get_count();
	Vertex *varray = va->get_mod_data();
	int tcount = mesh.get_triangle_array()->get_count();
	const Triangle *tptr = mesh.get_triangle_array()->get_data();

	
	Vector3 *utan = new Vector3[vcount];
	memset(utan, 0, vcount * sizeof(Vector3));

	Vector3 *vtan = new Vector3[vcount];
	memset(vtan, 0, vcount * sizeof(Vector3));

	for(int i=0; i<tcount; i++) {
		Vertex *v1 = &varray[tptr->vertices[0]];
		Vertex *v2 = &varray[tptr->vertices[1]];
		Vertex *v3 = &varray[tptr->vertices[2]];

		Vector3 vec1 = v2->pos - v1->pos;
		Vector3 vec2 = v3->pos - v1->pos;

		TexCoord tc1(v2->tex[0].u - v1->tex[0].u, v2->tex[0].v - v1->tex[0].v);
		TexCoord tc2(v3->tex[0].u - v1->tex[0].u, v3->tex[0].v - v1->tex[0].v);
	
		scalar_t r = 1.0 / (tc1.u * tc2.v - tc2.u * tc1.v);
		Vector3 udir(	(tc2.v * vec1.x - tc1.v * vec2.x) * r,
						(tc2.v * vec1.y - tc1.v * vec2.y) * r,
						(tc2.v * vec1.z - tc1.v * vec2.z) * r);

		Vector3 vdir(	(tc1.u * vec2.x - tc2.u * vec1.x) * r,
						(tc1.u * vec2.y - tc2.u * vec1.y) * r,
						(tc1.u * vec2.z - tc2.u * vec1.z) * r);

		utan[tptr->vertices[0]] += udir;
		utan[tptr->vertices[1]] += udir;
		utan[tptr->vertices[2]] += udir;
		
		vtan[tptr->vertices[0]] += vdir;
		vtan[tptr->vertices[1]] += vdir;
		vtan[tptr->vertices[2]] += vdir;
		
		tptr++;		
	}

	Vertex *vptr = varray;
	for(int i=0; i<vcount; i++) {
		Vector3 lvec = lpos - vptr->pos;

		Vector3 normal = -vptr->normal;
		Vector3 tan = utan[i];
		tan = (tan - normal * dot_product(normal, tan)).normalized();
		Vector3 bitan = cross_product(normal, tan);

		Basis tbn(tan, bitan, normal);
		lvec.transform(tbn.create_rotation_matrix());
		//lvec.normalize();
		
		vptr->tex[1].u = -lvec.z;
		vptr->tex[1].v = -lvec.y;
		vptr->tex[1].w = lvec.x;
		vptr++;
	}
	
	delete [] utan;
	delete [] vtan;
}


void Object::update_bounding_volume() {
	VertexStatistics vstat = mesh.get_vertex_stats();

	if(!bvol) {
		bvol = new BoundingSphere(vstat.centroid, vstat.max_dist);
		bvol_valid = true;
	} else {
		BoundingSphere *bsph;
		
		if((bsph = dynamic_cast<BoundingSphere*>(bvol))) {
			bsph->set_position(vstat.centroid);
			bsph->set_radius(vstat.max_dist);
			bvol_valid = true;
		} else {
			static int dbg;
			if(!dbg++) error("obj \"%s\": only bounding spheres are supported at this point", name.c_str());
		}
	}
}


// Convenience classes to deal with generated geometry

ObjPlane::ObjPlane(const Vector3 &normal, const Vector2 &size, int subdiv) {
	create_plane(get_mesh_ptr(), normal, size, subdiv);
}

ObjCylinder::ObjCylinder(scalar_t rad, scalar_t len, bool caps, int udiv, int vdiv) {
	create_cylinder(get_mesh_ptr(), rad, len, caps, udiv, vdiv);
}

ObjSphere::ObjSphere(scalar_t radius, int subdiv) {
	create_sphere(get_mesh_ptr(), radius, subdiv);
}

ObjTorus::ObjTorus(scalar_t circle_rad, scalar_t revolv_rad, int subdiv) {
	create_torus(get_mesh_ptr(), circle_rad, revolv_rad, subdiv);
}

ObjTeapot::ObjTeapot(scalar_t size, int subdiv) {
	create_teapot(get_mesh_ptr(), size, subdiv);
}
